iT邦幫忙

2024 iThome 鐵人賽

DAY 26
0
Modern Web

從零開始:全端新手的困境與成長系列 第 26

Day26 登入功能初體驗,JWT 的身份驗證流程!(下)

  • 分享至 

  • xImage
  •  

在完成後端 JWT 驗證後,前端該如何接收、存儲和攜帶這個 Token?在這篇文章中,我們將介紹如何在 Angular 中實現 JWT 驗證,包括如何處理登入狀態、發送帶有 Token 的 API 請求、保護頁面,還有一些前後端整合時應該注意的細節。這次我們會走得更細,確保每個步驟都解釋得清清楚楚!

https://ithelp.ithome.com.tw/upload/images/20241004/201683267ruer98VxM.png

文章大綱:

  1. JWT 與前端的密切關係
  2. 建立 Login 與 Home 頁面
  3. 在 Angular 前端處理 JWT
  4. 將 JWT 存入 Local Storage
  5. 將 JWT 存成 State
  6. 在前端攜帶 JWT 發送 API 請求
  7. Route Guard:保護特定頁面
  8. 處理跨域問題(CORS)
  9. 前後端無縫整合,安全高效的驗證系統

1. JWT 與前端的密切關係

在前一篇中,我們討論了 JWT 在後端的應用。JWT 是一種安全且簡單的方式來實現身份驗證,它能讓我們避免頻繁查詢資料庫。這篇文章將重點講解如何在前端處理這些 Token,並如何和後端進行驗證配合。JWT 在前端的任務就是接收伺服器發送的 Token,並在每次 API 請求時攜帶它。

在 Angular 中,我們會用 HttpClient 來發送請求,並在登入成功後將 Token 儲存在 Local Storage 或 State 中。這樣,我們就可以在後續的 API 請求中自動攜帶 Token,讓伺服器知道用戶的身份。


2. 建立 Login 與 Home 頁面

首先,我們會需要建立兩個基本的頁面:LoginComponentHomeComponent,這樣我們可以進行登入、跳轉首頁以及進行後續的 API 請求測試。

2-1. 建立 Login 和 Home Components

你可以使用 Angular CLI 來快速建立這兩個元件:

ng generate component login
ng generate component home

2-2. LoginComponent

login.component.html 中,你可以使用之前設定好的登入頁面。

也可以參考 Day12 掌握 Angular Material 更多元件與表單驗證:讓應用更強大

<!-- login.component.html -->
<div class="centered-container" fxLayout="column" fxLayoutAlign="center center">
  <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
    <mat-form-field appearance="fill">
      <mat-label>Email</mat-label>
      <input matInput formControlName="email" placeholder="輸入你的 Email" />
      <mat-error *ngIf="loginForm.controls['email'].hasError('required')">
        Email 是必填的
      </mat-error>
      <mat-error *ngIf="loginForm.controls['email'].hasError('email')">
        Email 格式不正確
      </mat-error>
    </mat-form-field>

    <mat-form-field appearance="fill">
      <mat-label>密碼</mat-label>
      <input
        matInput
        type="password"
        formControlName="password"
        placeholder="輸入你的密碼"
      />
      <mat-error *ngIf="loginForm.controls['password'].hasError('required')">
        密碼是必填的
      </mat-error>
    </mat-form-field>

    <button
      mat-raised-button
      color="primary"
      type="submit"
      [disabled]="!loginForm.valid"
    >
      登入
    </button>
  </form>
</div>

2-3. HomeComponent

當登入成功後,我們會跳轉到 HomeComponent。這是簡單的首頁,顯示一些商品,並且包含搜尋功能和購物車按鈕。

也可以參考 Day14 綜合 Angular Material 和 Flex Layout 製作簡單購物網站首頁

<!-- home.component.html -->
<mat-toolbar color="primary" fxLayout="row" fxLayoutAlign="space-around center">
  <div>
    <span>毛毛購物</span>
    <button mat-icon-button>
      <mat-icon>home</mat-icon>
    </button>
  </div>
  <mat-form-field fxFlex="50%">
    <mat-label>搜尋商品</mat-label>
    <input matInput placeholder="輸入商品名稱" />
  </mat-form-field>
  <button mat-icon-button>
    <mat-icon>shopping_cart</mat-icon>
  </button>
</mat-toolbar>

<div fxLayout="row" class="banner">
  <h1 fxFlex>本季熱銷商品!買一送一優惠中!</h1>
</div>

<div fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="center">
  <mat-card fxFlex="30%">
    <mat-card-header>
      <mat-card-title>毛毛舒適床!!!柔軟透氣寵物床</mat-card-title>
      <mat-card-subtitle>$100</mat-card-subtitle>
    </mat-card-header>
    <img mat-card-image src="assets/product1.jpg" alt="產品 1" />
    <mat-card-actions>
      <button mat-raised-button color="primary">加入購物車</button>
    </mat-card-actions>
  </mat-card>

  <mat-card fxFlex="30%">
    <mat-card-header>
      <mat-card-title>(⁎˃ᆺ˂)萌萌健康糧天然有機寵物飼料</mat-card-title>
      <mat-card-subtitle>$200</mat-card-subtitle>
    </mat-card-header>
    <img mat-card-image src="assets/product2.jpg" alt="產品 2" />
    <mat-card-actions>
      <button mat-raised-button color="primary">加入購物車</button>
    </mat-card-actions>
  </mat-card>

  <mat-card fxFlex="30%">
    <mat-card-header>
      <mat-card-title>毛寶潔牙棒-天然口腔清潔零食</mat-card-title>
      <mat-card-subtitle>$300</mat-card-subtitle>
    </mat-card-header>
    <img mat-card-image src="assets/product3.jpg" alt="產品 3" />
    <mat-card-actions>
      <button mat-raised-button color="primary">加入購物車</button>
    </mat-card-actions>
  </mat-card>
</div>

2-3. 設置路由(app-routing.module.ts)

要讓登入頁面和首頁能夠跳轉,我們需要設定路由。編輯 app-routing.module.ts,加入 LoginComponentHomeComponent 路徑。

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: 'home', component: HomeComponent },
  { path: '', redirectTo: '/login', pathMatch: 'full' }, // 預設重定向到 login 頁面
  { path: '**', redirectTo: '/login' }, // 未匹配的路由重定向到 login 頁面
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

3. 在 Angular 前端處理 JWT

登入成功後,後端會回傳一個 JWT。我們需要將這個 JWT 儲存起來,後續發送 API 請求時可以帶上這個 Token。接下來,我們將一步步來實作如何在前端處理 JWT。

3-1. 安裝 HttpClientModule

要發送 HTTP 請求,我們需要導入 Angular 內建的 HttpClientModule。確保在 app.module.ts 中導入並註冊它。

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [],
  imports: [HttpClientModule],
})
export class AppModule {}

3-2. 建立 AuthService 處理登入請求

AuthService 是一個服務,負責發送登入請求並接收後端回傳的 JWT。請在 src/app/services/auth.service.ts 中建立這個服務。

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  private apiUrl = 'http://localhost:3000'; // 你的後端 API 路徑

  constructor(private http: HttpClient) {}

  login(username: string, password: string): Observable<any> {
    return this.http.post(`${this.apiUrl}/login`, { username, password });
  }
}

這個服務會向後端發送 POST 請求,並回傳 JWT。


4. 將 JWT 存入 Local Storage

我們可以在登入成功後,將 JWT 儲存在 Local Storage,以便後續的 API 請求可以自動攜帶這個 Token。這部分的邏輯需要寫在 LoginComponent 的 onSubmit() 方法中。

4-1. 處理登入與存儲 Token

// src/app/login/login.component.ts
import { Component } from '@angular/core';
import { AuthService } from '../services/auth.service';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css'], // 添加這行來引用 CSS
})
export class LoginComponent {
  loginForm: FormGroup;

  constructor(
    private authService: AuthService,
    private router: Router,
    private fb: FormBuilder
  ) {
    this.loginForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', Validators.required],
    });
  }

  onSubmit() {
    const { email, password } = this.loginForm.value;

    this.authService.login(email, password).subscribe(
      (response) => {
        localStorage.setItem('token', response.token); // 將 JWT 存入 Local Storage
        this.router.navigate(['/home']); // 登入成功後跳轉到首頁
      },
      (error) => {
        console.error('登入失敗', error);
      }
    );
  }
}

這段程式碼在 LoginComponent 中處理登入邏輯,當登入成功時,將 JWT 存入 Local Storage,並跳轉到 HomeComponent。

https://ithelp.ithome.com.tw/upload/images/20241004/20168326HbEsP2BM5o.png


5. 將 JWT 存成 State

雖然我們已經將 JWT 儲存在 Local Storage 中,但依賴 Local Storage 的方式有時不夠靈活,尤其當我們希望即時追蹤用戶的登入狀態時。我們可以使用 Angular 的 BehaviorSubject 來將 JWT 存成應用程式的 State,這樣我們可以即時更新應用程式的狀態,而不用每次都從 Local Storage 中讀取。

這樣的做法能讓應用程式中的其他組件即時接收到登入狀態的變化,例如更新導航欄,顯示用戶是否已經登入等。

5-1. 使用 BehaviorSubject 存取 JWT

BehaviorSubject 是 RxJS 提供的一個非常實用的工具,它可以保存當前的狀態,並且當狀態變更時自動通知所有訂閱者。這非常適合用來管理應用程式中的登入狀態。

首先,我們會在 auth.service.ts 中實作一個 BehaviorSubject 來追蹤 JWT Token,並確保在登入或登出時能夠更新這個 State。

auth.service.ts

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  private apiUrl = 'http://localhost:3000'; // API 伺服器路徑
  private tokenSubject = new BehaviorSubject<string | null>(null); // 用來追蹤 JWT Token 的狀態

  constructor(private http: HttpClient) {
    const token = localStorage.getItem('token');
    if (token) {
      this.tokenSubject.next(token); // 如果 Local Storage 中有 Token,則存入 State
    }
  }

  // 讓其他組件能夠訂閱 Token 狀態變化
  get token$(): Observable<string | null> {
    return this.tokenSubject.asObservable();
  }

  // 處理登入請求並存入 Token
  login(username: string, password: string): Observable<any> {
    return this.http.post(`${this.apiUrl}/login`, { username, password }).pipe(
      tap((response: any) => {
        localStorage.setItem('token', response.token); // 將 JWT 存入 Local Storage
        this.tokenSubject.next(response.token); // 更新 State
      })
    );
  }

  // 登出並清除 Token
  logout() {
    localStorage.removeItem('token');
    this.tokenSubject.next(null); // 清除 State 中的 Token
  }
}

5-2. 如何在組件中訂閱這個 State

現在我們已經在 AuthService 中存入了 JWT 並將它存成 State,接下來我們可以在需要的組件中訂閱這個 State。這樣,當用戶登入或登出時,組件可以自動更新顯示的內容,例如導覽列上顯示的登入或登出按鈕。

app.component.ts

import { Component, OnInit } from '@angular/core';
import { AuthService } from './services/auth.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
  isLoggedIn = false; // 用來判斷使用者是否登入

  constructor(private authService: AuthService) {}

  ngOnInit(): void {
    // 訂閱 AuthService 的 Token State
    this.authService.token$.subscribe((token) => {
      this.isLoggedIn = !!token; // 如果有 Token,表示已登入
    });
  }

  logout(): void {
    this.authService.logout(); // 呼叫登出,清除 Token
  }
}

app.component.html

<mat-toolbar color="primary">
  <button mat-button *ngIf="!isLoggedIn" routerLink="/login">登入</button>
  <button mat-button *ngIf="isLoggedIn" (click)="logout()" routerLink="/login">
    登出
  </button>
</mat-toolbar>
<router-outlet></router-outlet>

在這裡,我們使用了 isLoggedIn 來控制登入和登出的按鈕顯示。當用戶登入時,我們會顯示登出按鈕;當用戶未登入時,我們會顯示登入按鈕。

https://ithelp.ithome.com.tw/upload/images/20241004/20168326d26AQhtv8D.png

5-3. 優點:即時更新應用狀態

這種做法的好處在於,我們不需要每次從 Local Storage 讀取 JWT,而是將它存成 State,並且讓應用程式的其他部分能夠即時追蹤登入狀態的變化。這對於大型應用來說,能夠提升性能並讓使用者體驗更加順暢。


6. 在前端攜帶 JWT 發送 API 請求

當用戶登入成功並儲存了 JWT 之後,接下來我們需要確保每次發送 API 請求時,都自動攜帶這個 Token。在 Angular 中,這可以透過 HttpInterceptor 來實現。攔截每次 HTTP 請求,並自動將 JWT 加入到請求的 Authorization Header 中。

6-1. 建立 auth.interceptor.ts

我們可以透過 Angular 的 HttpInterceptor 來攔截並修改每一個 HTTP 請求,將 JWT Token 加入到請求的標頭中。

// src/app/interceptors/auth.interceptor.ts

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = localStorage.getItem('token'); // 從 Local Storage 取得 JWT

    if (token) {
      const cloned = req.clone({
        headers: req.headers.set('Authorization', `Bearer ${token}`), // 加入 Authorization 標頭
      });
      return next.handle(cloned); // 攜帶 Token 發送請求
    } else {
      return next.handle(req); // 如果沒有 Token,直接處理原請求
    }
  }
}

6-2. 在 app.module.ts 中註冊 Interceptor

記得將這個 Interceptor 註冊到應用中,以便每次 HTTP 請求都會自動攜帶 JWT Token。

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './services/auth.interceptor';

@NgModule({
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
  ],
})
export class AppModule {}

https://ithelp.ithome.com.tw/upload/images/20241004/201683269Tqwy8TJrj.png


7. Route Guard:保護特定頁面

在應用中,我們可能會有一些頁面僅限於已登入的用戶訪問,例如個人資料頁面、管理員頁面等。我們可以使用 Route Guard 來保護這些頁面,確保只有已登入的用戶才能存取。

7-1. 建立 auth.guard.ts

我們將使用 Angular 的 CanActivate 來保護路由。每當用戶試圖訪問受保護的頁面時,Angular 會透過這個 Guard 來檢查用戶是否已經登入並持有有效的 JWT Token。如果未登入,則重導到登入頁面。

檔案路徑:src/app/guards/auth.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../services/auth.service'; // 請確認路徑與 AuthService 檔案位置一致

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(): boolean {
    const token = localStorage.getItem('token'); // 從 Local Storage 取得 JWT
    if (token) {
      return true; // 已登入,允許進入頁面
    } else {
      this.router.navigate(['/login']); // 未登入,重導至登入頁面
      return false; // 拒絕進入頁面
    }
  }
}

7-2. 在路由中使用 AuthGuard

接下來,我們需要將這個 Guard 應用到我們想保護的路由上,這樣當用戶未登入時,就會自動重導到登入頁面。

首先,找到你的路由檔案 app-routing.module.ts,並在其中使用 AuthGuard 來保護路由。

檔案路徑:src/app/app-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';
import { AuthGuard } from './guards/auth.guard';

const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: 'home', component: HomeComponent, canActivate: [AuthGuard] }, // 保護首頁
  { path: '', redirectTo: '/login', pathMatch: 'full' }, // 預設重定向到 login 頁面
  { path: '**', redirectTo: '/login' }, // 未匹配的路由重定向到 login 頁面
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

7-3. 如何應用 AuthGuard

在這裡,我們將 AuthGuard 應用到 /home 路由上。當用戶訪問這些路徑時,系統會先檢查 Local Storage 中是否有 JWT。如果有,則允許進入頁面,否則重導至 /login

如果你在訪問受保護的頁面時沒有攜帶 Token,Angular 會自動攔截,並重導使用者到登入頁面,這樣就能確保受保護的頁面不會被未登入的使用者看到。


8. 處理跨域問題(CORS)

JWT 身份驗證的核心在於前後端的無縫整合,這裡有幾個重要的細節需要注意。

安裝 CORS 套件

你可以使用 cors 這個 Node.js 的中介軟體來解決跨域問題。首先,我們需要安裝這個套件。

npm install cors

處理跨域問題(CORS)

當前後端分開部署時,可能會遇到 CORS(跨域資源共享) 問題,這會導致前端無法向後端發送請求。為了避免這個問題,我們需要在後端設置 CORS 許可,允許特定的前端域名訪問 API。

Express 中,可以使用 cors 套件來解決這個問題:

const cors = require('cors');
app.use(cors({ origin: 'http://localhost:4200' })); // 允許 Angular 本地伺服器發送請求

10. 前後端無縫整合,安全高效的驗證系統

這篇文章詳細講解了如何在前端實現 JWT 登入驗證,以及前後端如何整合這個流程。從登入畫面的設定、Token 的儲存與管理、到攜帶 JWT 發送 API 請求,我們一步步展示了如何確保用戶的身份安全和 API 的受保護。

透過這樣的流程,你可以在開發過程中建立一個更安全、可擴展的身份驗證系統,讓你的應用程式更加穩定。接下來,你可以根據這些流程,開始實現更多受保護的功能,並針對不同使用者設計專屬的權限控管系統。


上一篇
Day25 登入功能初體驗,JWT 的身份驗證流程!(上)
下一篇
Day27 密碼驗證流程 – MySQL 中的密碼儲存與檢查
系列文
從零開始:全端新手的困境與成長30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言